// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.fx.AbstractDragDropTest');
goog.setTestOnly('goog.fx.AbstractDragDropTest');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('goog.fx.AbstractDragDrop');
goog.require('goog.fx.DragDropItem');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');

const targets = [
  {box_: new goog.math.Box(0, 3, 1, 1)}, {box_: new goog.math.Box(0, 7, 2, 6)},
  {box_: new goog.math.Box(2, 2, 3, 1)}, {box_: new goog.math.Box(4, 1, 6, 1)},
  {box_: new goog.math.Box(4, 9, 7, 6)}, {box_: new goog.math.Box(9, 9, 10, 1)}
];

const targets2 = [
  {box_: new goog.math.Box(10, 50, 20, 10)},
  {box_: new goog.math.Box(20, 50, 30, 10)},
  {box_: new goog.math.Box(60, 50, 70, 10)},
  {box_: new goog.math.Box(70, 50, 80, 10)}
];

const targets3 = [
  {box_: new goog.math.Box(0, 4, 1, 1)}, {box_: new goog.math.Box(1, 6, 4, 5)},
  {box_: new goog.math.Box(5, 5, 6, 2)}, {box_: new goog.math.Box(2, 1, 5, 0)}
];


/**
 * Test the utility function which tells how two one dimensional ranges
 * overlap.
 */
function testRangeOverlap() {
  assertEquals(RangeOverlap.LEFT, rangeOverlap(1, 2, 3, 4));
  assertEquals(RangeOverlap.LEFT, rangeOverlap(2, 3, 3, 4));
  assertEquals(RangeOverlap.LEFT_IN, rangeOverlap(1, 3, 2, 4));
  assertEquals(RangeOverlap.IN, rangeOverlap(1, 3, 1, 4));
  assertEquals(RangeOverlap.IN, rangeOverlap(2, 3, 1, 4));
  assertEquals(RangeOverlap.IN, rangeOverlap(3, 4, 1, 4));
  assertEquals(RangeOverlap.RIGHT_IN, rangeOverlap(2, 4, 1, 3));
  assertEquals(RangeOverlap.RIGHT, rangeOverlap(2, 3, 1, 2));
  assertEquals(RangeOverlap.RIGHT, rangeOverlap(3, 4, 1, 2));
  assertEquals(RangeOverlap.CONTAINS, rangeOverlap(1, 4, 2, 3));
}


/**
 * An enum describing how two ranges overlap (non-symmetrical relation).
 * @enum {number}
 */
RangeOverlap = {
  LEFT: 1,      // First range is placed to the left of the second.
  LEFT_IN: 2,   // First range overlaps on the left side of the second.
  IN: 3,        // First range is completely contained in the second.
  RIGHT_IN: 4,  // First range overlaps on the right side of the second.
  RIGHT: 5,     // First range is placed to the right side of the second.
  CONTAINS: 6   // First range contains the second.
};


/**
 * Computes how two one dimensional ranges overlap.
 *
 * @param {number} left1 Left inclusive bound of the first range.
 * @param {number} right1 Right exclusive bound of the first range.
 * @param {number} left2 Left inclusive bound of the second range.
 * @param {number} right2 Right exclusive bound of the second range.
 * @return {RangeOverlap} The enum value describing the type of the overlap.
 */
function rangeOverlap(left1, right1, left2, right2) {
  if (right1 <= left2) return RangeOverlap.LEFT;
  if (left1 >= right2) return RangeOverlap.RIGHT;
  const leftIn = left1 >= left2;
  const rightIn = right1 <= right2;
  if (leftIn && rightIn) return RangeOverlap.IN;
  if (leftIn) return RangeOverlap.RIGHT_IN;
  if (rightIn) return RangeOverlap.LEFT_IN;
  return RangeOverlap.CONTAINS;
}


/**
 * Tells whether two boxes overlap.
 *
 * @param {goog.math.Box} box1 First box in question.
 * @param {goog.math.Box} box2 Second box in question.
 * @return {boolean} Whether boxes overlap in any way.
 */
function boxOverlaps(box1, box2) {
  const horizontalOverlap =
      rangeOverlap(box1.left, box1.right, box2.left, box2.right);
  const verticalOverlap =
      rangeOverlap(box1.top, box1.bottom, box2.top, box2.bottom);
  return horizontalOverlap != RangeOverlap.LEFT &&
      horizontalOverlap != RangeOverlap.RIGHT &&
      verticalOverlap != RangeOverlap.LEFT &&
      verticalOverlap != RangeOverlap.RIGHT;
}


/**
 * Tests if the utility function to compute box overlapping functions properly.
 */
function testBoxOverlaps() {
  // Overlapping tests.
  let box2 = new goog.math.Box(1, 4, 4, 1);

  // Corner overlaps.
  assertTrue('NW overlap', boxOverlaps(new goog.math.Box(0, 2, 2, 0), box2));
  assertTrue('NE overlap', boxOverlaps(new goog.math.Box(0, 5, 2, 3), box2));
  assertTrue('SE overlap', boxOverlaps(new goog.math.Box(3, 5, 5, 3), box2));
  assertTrue('SW overlap', boxOverlaps(new goog.math.Box(3, 2, 5, 0), box2));

  // Inside.
  assertTrue(
      'Inside overlap', boxOverlaps(new goog.math.Box(2, 3, 3, 2), box2));

  // Around.
  assertTrue(
      'Outside overlap', boxOverlaps(new goog.math.Box(0, 5, 5, 0), box2));

  // Edge overlaps.
  assertTrue('N overlap', boxOverlaps(new goog.math.Box(0, 3, 2, 2), box2));
  assertTrue('E overlap', boxOverlaps(new goog.math.Box(2, 5, 3, 3), box2));
  assertTrue('S overlap', boxOverlaps(new goog.math.Box(3, 3, 5, 2), box2));
  assertTrue('W overlap', boxOverlaps(new goog.math.Box(2, 2, 3, 0), box2));

  assertTrue('N-in overlap', boxOverlaps(new goog.math.Box(0, 5, 2, 0), box2));
  assertTrue('E-in overlap', boxOverlaps(new goog.math.Box(0, 5, 5, 3), box2));
  assertTrue('S-in overlap', boxOverlaps(new goog.math.Box(3, 5, 5, 0), box2));
  assertTrue('W-in overlap', boxOverlaps(new goog.math.Box(0, 2, 5, 0), box2));

  // Does not overlap.
  box2 = new goog.math.Box(3, 6, 6, 3);

  // Along the edge - shorter.
  assertFalse(
      'N-in no overlap', boxOverlaps(new goog.math.Box(1, 5, 2, 4), box2));
  assertFalse(
      'E-in no overlap', boxOverlaps(new goog.math.Box(4, 8, 5, 7), box2));
  assertFalse(
      'S-in no overlap', boxOverlaps(new goog.math.Box(7, 5, 8, 4), box2));
  assertFalse(
      'N-in no overlap', boxOverlaps(new goog.math.Box(4, 2, 5, 1), box2));

  // By the corner.
  assertFalse(
      'NE no overlap', boxOverlaps(new goog.math.Box(1, 8, 2, 7), box2));
  assertFalse(
      'SE no overlap', boxOverlaps(new goog.math.Box(7, 8, 8, 7), box2));
  assertFalse(
      'SW no overlap', boxOverlaps(new goog.math.Box(7, 2, 8, 1), box2));
  assertFalse(
      'NW no overlap', boxOverlaps(new goog.math.Box(1, 2, 2, 1), box2));

  // Perpendicular to an edge.
  assertFalse(
      'NNE no overlap', boxOverlaps(new goog.math.Box(1, 7, 2, 5), box2));
  assertFalse(
      'NEE no overlap', boxOverlaps(new goog.math.Box(2, 8, 4, 7), box2));
  assertFalse(
      'SEE no overlap', boxOverlaps(new goog.math.Box(5, 8, 7, 7), box2));
  assertFalse(
      'SSE no overlap', boxOverlaps(new goog.math.Box(7, 7, 8, 5), box2));
  assertFalse(
      'SSW no overlap', boxOverlaps(new goog.math.Box(7, 4, 8, 2), box2));
  assertFalse(
      'SWW no overlap', boxOverlaps(new goog.math.Box(5, 2, 7, 1), box2));
  assertFalse(
      'NWW no overlap', boxOverlaps(new goog.math.Box(2, 2, 4, 1), box2));
  assertFalse(
      'NNW no overlap', boxOverlaps(new goog.math.Box(1, 4, 2, 2), box2));

  // Along the edge - longer.
  assertFalse('N no overlap', boxOverlaps(new goog.math.Box(0, 7, 1, 2), box2));
  assertFalse('E no overlap', boxOverlaps(new goog.math.Box(2, 9, 7, 8), box2));
  assertFalse('S no overlap', boxOverlaps(new goog.math.Box(8, 7, 9, 2), box2));
  assertFalse('W no overlap', boxOverlaps(new goog.math.Box(2, 1, 7, 0), box2));
}


/**
 * Checks whether a given box overlaps any of given DnD target boxes.
 *
 * @param {goog.math.Box} box The box to check.
 * @param {Array<Object>} targets The array of targets with boxes to check
 *     if they overlap with the given box.
 * @return {boolean} Whether the box overlaps any of the target boxes.
 */
function boxOverlapsTargets(box, targets) {
  return goog.array.some(
      targets, function(target) { return boxOverlaps(box, target.box_); });
}


function testMaybeCreateDummyTargetForPosition() {
  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets;
  testGroup.targetBox_ = new goog.math.Box(0, 9, 10, 1);

  let target = testGroup.maybeCreateDummyTargetForPosition_(3, 3);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(3, 3, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(2, 4);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(2, 4, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(2, 7);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(2, 7, target.box_));

  testGroup.targetList_.push({box_: new goog.math.Box(5, 6, 6, 0)});

  target = testGroup.maybeCreateDummyTargetForPosition_(3, 3);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(3, 3, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(2, 7);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(2, 7, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(6, 3);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(6, 3, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(0, 3);
  assertNull(target);
  target = testGroup.maybeCreateDummyTargetForPosition_(9, 0);
  assertNull(target);
}


function testMaybeCreateDummyTargetForPosition2() {
  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets2;
  testGroup.targetBox_ = new goog.math.Box(10, 50, 80, 10);

  let target = testGroup.maybeCreateDummyTargetForPosition_(30, 40);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(30, 40, target.box_));

  target = testGroup.maybeCreateDummyTargetForPosition_(45, 40);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(45, 40, target.box_));

  testGroup.targetList_.push({box_: new goog.math.Box(40, 50, 50, 40)});

  target = testGroup.maybeCreateDummyTargetForPosition_(30, 40);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  target = testGroup.maybeCreateDummyTargetForPosition_(45, 35);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
}


function testMaybeCreateDummyTargetForPosition3BoxHasDecentSize() {
  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets3;
  testGroup.targetBox_ = new goog.math.Box(0, 6, 6, 0);

  const target = testGroup.maybeCreateDummyTargetForPosition_(3, 3);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(3, 3, target.box_));
  assertEquals('(1t, 5r, 5b, 1l)', target.box_.toString());
}


function testMaybeCreateDummyTargetForPosition4() {
  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets;
  testGroup.targetBox_ = new goog.math.Box(0, 9, 10, 1);

  for (let x = testGroup.targetBox_.left; x < testGroup.targetBox_.right; x++) {
    for (let y = testGroup.targetBox_.top; y < testGroup.targetBox_.bottom;
         y++) {
      let inRealTarget = false;
      for (let i = 0; i < testGroup.targetList_.length; i++) {
        if (testGroup.isInside(x, y, testGroup.targetList_[i].box_)) {
          inRealTarget = true;
          break;
        }
      }
      if (!inRealTarget) {
        const target = testGroup.maybeCreateDummyTargetForPosition_(x, y);
        if (target) {
          assertFalse(
              'Fake target for point(' + x + ',' + y + ') should ' +
                  'not overlap any real targets.',
              boxOverlapsTargets(target.box_, testGroup.targetList_));
          assertTrue(testGroup.isInside(x, y, target.box_));
        }
      }
    }
  }
}

function testMaybeCreateDummyTargetForPosition_NegativePositions() {
  const negTargets = [
    {box_: new goog.math.Box(-20, 10, -5, 1)},
    {box_: new goog.math.Box(20, 10, 30, 1)}
  ];

  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = negTargets;
  testGroup.targetBox_ = new goog.math.Box(-20, 10, 30, 1);

  const target = testGroup.maybeCreateDummyTargetForPosition_(1, 5);
  assertFalse(boxOverlapsTargets(target.box_, testGroup.targetList_));
  assertTrue(testGroup.isInside(1, 5, target.box_));
}

function testMaybeCreateDummyTargetOutsideScrollableContainer() {
  const targets = [
    {box_: new goog.math.Box(0, 3, 10, 1)},
    {box_: new goog.math.Box(20, 3, 30, 1)}
  ];
  const target = targets[0];

  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets;
  testGroup.targetBox_ = new goog.math.Box(0, 3, 30, 1);

  testGroup.addScrollableContainer(document.getElementById('container1'));
  const container = testGroup.scrollableContainers_[0];
  container.containedTargets_.push(target);
  container.box_ = new goog.math.Box(0, 3, 5, 1);  // shorter than target
  target.scrollableContainer_ = container;

  // mouse cursor is below scrollable target but not the actual target
  const dummyTarget = testGroup.maybeCreateDummyTargetForPosition_(2, 7);
  // dummy target should not overlap the scrollable container
  assertFalse(boxOverlaps(dummyTarget.box_, container.box_));
  // but should overlap the actual target, since not all of it is visible
  assertTrue(boxOverlaps(dummyTarget.box_, target.box_));
}

function testMaybeCreateDummyTargetInsideScrollableContainer() {
  const targets = [
    {box_: new goog.math.Box(0, 3, 10, 1)},
    {box_: new goog.math.Box(20, 3, 30, 1)}
  ];
  const target = targets[0];

  const testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = targets;
  testGroup.targetBox_ = new goog.math.Box(0, 3, 30, 1);

  testGroup.addScrollableContainer(document.getElementById('container1'));
  const container = testGroup.scrollableContainers_[0];
  container.containedTargets_.push(target);
  container.box_ = new goog.math.Box(0, 3, 20, 1);  // longer than target
  target.scrollableContainer_ = container;

  // mouse cursor is below both the scrollable and the actual target
  const dummyTarget = testGroup.maybeCreateDummyTargetForPosition_(2, 15);
  // dummy target should overlap the scrollable container
  assertTrue(boxOverlaps(dummyTarget.box_, container.box_));
  // but not overlap the actual target
  assertFalse(boxOverlaps(dummyTarget.box_, target.box_));
}

function testCalculateTargetBox() {
  let testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = [];
  goog.array.forEach(targets, function(target) {
    testGroup.targetList_.push(target);
    testGroup.calculateTargetBox_(target.box_);
  });
  assertTrue(
      goog.math.Box.equals(
          testGroup.targetBox_, new goog.math.Box(0, 9, 10, 1)));

  testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = [];
  goog.array.forEach(targets2, function(target) {
    testGroup.targetList_.push(target);
    testGroup.calculateTargetBox_(target.box_);
  });
  assertTrue(
      goog.math.Box.equals(
          testGroup.targetBox_, new goog.math.Box(10, 50, 80, 10)));

  testGroup = new goog.fx.AbstractDragDrop();
  testGroup.targetList_ = [];
  goog.array.forEach(targets3, function(target) {
    testGroup.targetList_.push(target);
    testGroup.calculateTargetBox_(target.box_);
  });
  assertTrue(
      goog.math.Box.equals(
          testGroup.targetBox_, new goog.math.Box(0, 6, 6, 0)));
}


function testIsInside() {
  const add = new goog.fx.AbstractDragDrop();
  // The box in question.
  // 10,20+++++20,20
  //   +         |
  // 10,30-----20,30
  const box = new goog.math.Box(20, 20, 30, 10);

  assertTrue(
      'A point somewhere in the middle of the box should be inside.',
      add.isInside(15, 25, box));

  assertTrue(
      'A point in top-left corner should be inside the box.',
      add.isInside(10, 20, box));

  assertTrue(
      'A point on top border should be inside the box.',
      add.isInside(15, 20, box));

  assertFalse(
      'A point in top-right corner should be outside the box.',
      add.isInside(20, 20, box));

  assertFalse(
      'A point on right border should be outside the box.',
      add.isInside(20, 25, box));

  assertFalse(
      'A point in bottom-right corner should be outside the box.',
      add.isInside(20, 30, box));

  assertFalse(
      'A point on bottom border should be outside the box.',
      add.isInside(15, 30, box));

  assertFalse(
      'A point in bottom-left corner should be outside the box.',
      add.isInside(10, 30, box));

  assertTrue(
      'A point on left border should be inside the box.',
      add.isInside(10, 25, box));

  add.dispose();
}


function testAddingRemovingScrollableContainers() {
  const group = new goog.fx.AbstractDragDrop();
  const el1 = goog.dom.createElement(goog.dom.TagName.DIV);
  const el2 = goog.dom.createElement(goog.dom.TagName.DIV);

  assertEquals(0, group.scrollableContainers_.length);

  group.addScrollableContainer(el1);
  assertEquals(1, group.scrollableContainers_.length);

  group.addScrollableContainer(el2);
  assertEquals(2, group.scrollableContainers_.length);

  group.removeAllScrollableContainers();
  assertEquals(0, group.scrollableContainers_.length);
}


function testScrollableContainersCalculation() {
  const group = new goog.fx.AbstractDragDrop();
  const target = new goog.fx.AbstractDragDrop();

  group.addTarget(target);
  group.addScrollableContainer(document.getElementById('container1'));
  const container = group.scrollableContainers_[0];

  const item1 = new goog.fx.DragDropItem(document.getElementById('child1'));
  const item2 = new goog.fx.DragDropItem(document.getElementById('child2'));

  target.items_.push(item1);
  group.recalculateDragTargets();
  group.recalculateScrollableContainers();

  assertEquals(1, container.containedTargets_.length);
  assertEquals(container, group.targetList_[0].scrollableContainer_);

  target.items_.push(item2);
  group.recalculateDragTargets();
  assertEquals(1, container.containedTargets_.length);
  assertNull(group.targetList_[0].scrollableContainer_);

  group.recalculateScrollableContainers();
  assertEquals(2, container.containedTargets_.length);
  assertEquals(container, group.targetList_[1].scrollableContainer_);
}

function testMouseDownEventDefaultAction() {
  const group = new goog.fx.AbstractDragDrop();
  const target = new goog.fx.AbstractDragDrop();
  group.addTarget(target);
  const item1 = new goog.fx.DragDropItem(document.getElementById('child1'));
  group.items_.push(item1);
  item1.setParent(group);
  group.init();

  const mousedownDefaultPrevented =
      !goog.testing.events.fireMouseDownEvent(item1.element);

  assertFalse(
      'Default action of mousedown event should not be cancelled.',
      mousedownDefaultPrevented);
}

// See http://b/7494613.
function testMouseUpOutsideElement() {
  const group = new goog.fx.AbstractDragDrop();
  const target = new goog.fx.AbstractDragDrop();
  group.addTarget(target);
  const item1 = new goog.fx.DragDropItem(document.getElementById('child1'));
  group.items_.push(item1);
  item1.setParent(group);
  group.init();

  group.startDrag = goog.functions.error('startDrag should not be called.');

  goog.testing.events.fireMouseDownEvent(item1.element);
  goog.testing.events.fireMouseUpEvent(item1.element.parentNode);
  // This should have no effect (not start a drag) since the previous event
  // should have cleared the listeners.
  goog.testing.events.fireMouseOutEvent(item1.element);

  group.dispose();
  target.dispose();
}

function testScrollBeforeMoveDrag() {
  const group = new goog.fx.AbstractDragDrop();
  const target = new goog.fx.AbstractDragDrop();

  group.addTarget(target);
  const container = document.getElementById('container1');
  group.addScrollableContainer(container);

  const childEl = document.getElementById('child1');
  const item = new goog.fx.DragDropItem(childEl);
  item.currentDragElement_ = childEl;

  target.items_.push(item);
  group.recalculateDragTargets();
  group.recalculateScrollableContainers();

  // Simulare starting a drag.
  const moveEvent = {
    'clientX': 8,
    'clientY': 10,
    'type': goog.events.EventType.MOUSEMOVE,
    'relatedTarget': childEl,
    'preventDefault': function() {}
  };
  group.startDrag(moveEvent, item);

  // Simulate scrolling before the first move drag event.
  const scrollEvent = {'target': container};
  assertNotThrows(goog.bind(group.containerScrollHandler_, group, scrollEvent));
}


function testMouseMove_mouseOutBeforeThreshold() {
  // Setup dragdrop and item
  const itemEl = goog.dom.createElement(goog.dom.TagName.DIV);
  const childEl = goog.dom.createElement(goog.dom.TagName.DIV);
  itemEl.appendChild(childEl);
  const add = new goog.fx.AbstractDragDrop();
  const item = new goog.fx.DragDropItem(itemEl);
  item.setParent(add);
  add.items_.push(item);

  // Simulate maybeStartDrag
  item.startPosition_ = new goog.math.Coordinate(10, 10);
  item.currentDragElement_ = itemEl;

  // Test
  let draggedItem = null;
  add.startDrag = function(event, item) { draggedItem = item; };

  let event =
      new goog.testing.events.Event(goog.events.EventType.MOUSEOUT, childEl);
  // Drag distance is only 2.
  event.clientX = 8;
  event.clientY = 10;
  item.mouseMove_(event);
  assertEquals(
      'DragStart should not be fired for mouseout on child element.', null,
      draggedItem);

  event = new goog.testing.events.Event(goog.events.EventType.MOUSEOUT, itemEl);
  // Drag distance is only 2.
  event.clientX = 8;
  event.clientY = 10;
  item.mouseMove_(event);
  assertEquals(
      'DragStart should be fired for mouseout on main element.', item,
      draggedItem);
}


function testGetDragElementPosition() {
  const testGroup = new goog.fx.AbstractDragDrop();
  const sourceEl = goog.dom.createElement(goog.dom.TagName.DIV);
  document.body.appendChild(sourceEl);

  let pageOffset = goog.style.getPageOffset(sourceEl);
  let pos = testGroup.getDragElementPosition(sourceEl);
  assertEquals(
      'Drag element position should be source element page offset',
      pageOffset.x, pos.x);
  assertEquals(
      'Drag element position should be source element page offset',
      pageOffset.y, pos.y);

  sourceEl.style.marginLeft = '5px';
  sourceEl.style.marginTop = '7px';
  pageOffset = goog.style.getPageOffset(sourceEl);
  pos = testGroup.getDragElementPosition(sourceEl);
  assertEquals(
      'Drag element position should be adjusted for source element ' +
          'margins',
      pageOffset.x - 10, pos.x);
  assertEquals(
      'Drag element position should be adjusted for source element ' +
          'margins',
      pageOffset.y - 14, pos.y);
}

function testDragEndEvent() {
  function testDragEndEventInternal(shouldContainItemData) {
    const testGroup = new goog.fx.AbstractDragDrop();

    const childEl = document.getElementById('child1');
    const item = new goog.fx.DragDropItem(childEl);
    item.currentDragElement_ = childEl;

    testGroup.items_.push(item);
    testGroup.recalculateDragTargets();

    // Simulate starting a drag
    const startEvent = {
      'clientX': 0,
      'clientY': 0,
      'type': goog.events.EventType.MOUSEMOVE,
      'relatedTarget': childEl,
      'preventDefault': function() {}
    };
    testGroup.startDrag(startEvent, item);

    testGroup.activeTarget_ = new goog.fx.ActiveDropTarget_(
        new goog.math.Box(0, 0, 0, 0), testGroup, item, childEl);

    goog.events.listen(
        testGroup, goog.fx.AbstractDragDrop.EventType.DRAGEND, function(event) {
          if (shouldContainItemData) {
            assertEquals(
                'The drag end event should contain a drop target', testGroup,
                event.dropTarget);
            assertEquals(
                'The drag end event should contain a drop target item', item,
                event.dropTargetItem);
            assertEquals(
                'The drag end event should contain a drop target element',
                childEl, event.dropTargetElement);
          } else {
            assertUndefined(
                'The drag end event shouldn\'t contain a drop target',
                event.dropTarget);
            assertUndefined(
                'The drag end event shouldn\'t contain a drop target item',
                event.dropTargetItem);
            assertUndefined(
                'The drag end event shouldn\'t contain a drop target element',
                event.dropTargetElement);
          }
        });

    testGroup.endDrag(
        {'clientX': 0, 'clientY': 0, 'dragCanceled': !shouldContainItemData});

    testGroup.dispose();
    item.dispose();
  }

  testDragEndEventInternal(false);
  testDragEndEventInternal(true);
}

function testDropEventHasBrowserEvent() {
  const testGroup = new goog.fx.AbstractDragDrop();

  const childEl = document.getElementById('child1');
  const item = new goog.fx.DragDropItem(childEl);
  item.currentDragElement_ = childEl;

  testGroup.items_.push(item);
  testGroup.recalculateDragTargets();

  // Simulate starting a drag
  const startBrowserEvent = {
    'clientX': 0,
    'clientY': 0,
    'type': goog.events.EventType.MOUSEMOVE,
    'relatedTarget': childEl,
    'preventDefault': function() {},
  };
  testGroup.startDrag(startBrowserEvent, item);

  testGroup.activeTarget_ = new goog.fx.ActiveDropTarget_(
      new goog.math.Box(0, 0, 0, 0), testGroup, item, childEl);

  const endBrowserEvent = {
    'clientX': 0,
    'clientY': 0,
    'type': goog.events.EventType.MOUSEUP,
    'ctrlKey': false,
    'altKey': true
  };

  goog.events.listen(
      testGroup, goog.fx.AbstractDragDrop.EventType.DROP, function(event) {
        const browserEvent = event.browserEvent;
        assertEquals(
            'The drop event should contain the browser event', endBrowserEvent,
            browserEvent);
      });

  testGroup.endDrag({
    'clientX': 0,
    'clientY': 0,
    'dragCanceled': false,
    'browserEvent': endBrowserEvent
  });

  testGroup.dispose();
  item.dispose();
}

// Helper function for manual debugging.
function drawTargets(targets, multiplier) {
  const colors = ['green', 'blue', 'red', 'lime', 'pink', 'silver', 'orange'];
  const cont = document.getElementById('cont');
  cont.innerHTML = '';
  for (let i = 0; i < targets.length; i++) {
    const box = targets[i].box_;
    const el = goog.dom.createElement(goog.dom.TagName.DIV);
    el.style.top = (box.top * multiplier) + 'px';
    el.style.left = (box.left * multiplier) + 'px';
    el.style.width = ((box.right - box.left) * multiplier) + 'px';
    el.style.height = ((box.bottom - box.top) * multiplier) + 'px';
    el.style.backgroundColor = colors[i];
    cont.appendChild(el);
  }
}
